/*                                  ATELIER                                   */

/* %%LICENSE_TAG%%                                                            */
package edu.gmu.view.canvas;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Vector;
import javax.swing.JComponent;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/**
 * The Canvas is a Component that delegates updating and
 * painting to Layer objects.  These Layer objects are very
 * lightweight when compared to creating many Components overlayed
 * on top of each other (e.g. Adding many transparent JPanels
 * to a JComponent).
 * <p>
 * With this approach, we only have one Component to draw on and 
 * we explicitly control the ordering of the Layers.
 * <p>
 * We hold a reference to all the Layers.  We also hold
 * the MouseMode which determines how MouseEvents are handled.
 * <p>
 * The Canvas object queues a task to call the paintLayer
 * method on each Layer at the specified frame rate.  Note
 * the order that paintLayer calls the Layer - index[0]
 * is drawn last - i.e. it is on top of the other Layers.
 */
public class Canvas extends JComponent
{
    /**
     * Default frameRate
     */
    protected static final int DEFAULT_FRAME_RATE = 10;
    
    /**
     * List of Layers
     */
    protected LayerModel layerModel = new LayerModel( );
        
    /**
     * Frame Rate in frames per second
     */
    protected int frameRate = DEFAULT_FRAME_RATE;
    
    /**
     * Timer interval - determined from frame rate
     */
    protected int timerInterval = 100;
    
    /**
     * Collection to hold our listeners.
     */
    protected Vector<MouseModeListener> mouseModeListeners = new Vector<MouseModeListener>();
    
    /**
     * MouseMode for this AnimatedComponent.  there can be only one MouseMode at a time
     * for the AnimatedComponent - it is "active", when another one is set, the new one
     * becomes active and the other inactive.
     */
    protected MouseMode mouseMode = null;
    
    /**
     * Background Color
     */
    protected Color backgroundColor = Color.BLACK;
    
    /**
     * Disabled Color
     */
    protected Color disabledColor = Color.GRAY;
    
    /**
     * Background Layer
     */
    protected BackgroundLayer background = new BackgroundLayer( backgroundColor );
    
    /**
     * ProjectionThread used to execute the animationLoop which calls
     * projectLayer and then queues the EDT to call paintLayer.
     */
    protected Timer updateThread = null;
    
    /**
     * Creates a new AnimatedComponent with default values.
     */
    public Canvas( )
    {
        this( DEFAULT_FRAME_RATE );
    }
    
    /**
     * Creates a new AnimatedComponent with specified values.
     * 
     * 
     * @param frameRate
     */
    public Canvas( int frameRate )
    {
        super( );
        
        /*
         * Set the UI - this sets default Fonts/Colors
         */
        this.updateUI();

        /*
         * Determine the timerInterval from frameRate
         */
        this.frameRate = frameRate;
        this.timerInterval = calculateInterval( frameRate );
        
        /*
         * We are opaque per the JComponent.paintComponent contract
         */
        this.setOpaque( true );
        
        /*
         * Turn off double-buffering - this means that it is up 
         * to us to prevent flicker by not rendering an image
         * until it is completely drawn - i.e.using a backbuffer
         * to draw to them rendering it when finished.
         */
        this.setDoubleBuffered( false );
        
        /*
         * Set the default MouseMode
         */
        this.setMouseMode( new LayerMouseMode(this) );
    }
    
    
    /**
     *
     * @param color
     */
    public void setBackgroundColor(Color color) {
        this.background.setColor(color);
    }
    
    /**
     * Determines timerInterval from frameRate.
     *
     * @param frameRate
     * @return timerInterval to sleep in milli-seconds to achive close to given  frameRate
     */
    protected static int calculateInterval( int frameRate )
    {
        float floatInterval = 1000.0f / frameRate;
        return (int)floatInterval;
    }
    
    /**
     * Starts the AnimatedComponent Updating and Painting its Layers.  Subsequent
     * calls to this method without first calling stop will simply return
     * without doing anything.
     */
    public void start( )
    {
        /*
         * If we have already been started - ignore request or complain?
         */
        if( this.updateThread != null )
        {
            String m = "Animation has already been started - call stop before calling this method.";
            throw new IllegalArgumentException( m );
        }
        
        /*
         * Make daemon Thread so we do not hang a graceful shutdown
         */
        this.updateThread = new Timer( "UpdateThread", true );
        
        final Runnable runner = new Runnable()
        {
            public void run( )
            {
                /*
                 * NOTE: Called in the EDT
                 */
                paintLayers( );
            }
        };
        
        TimerTask timerTask = new TimerTask()
        {
            public void run()
            {
                /*
                 * Note that I used to use isVisible - and it sometimes
                 * returned false when I thought it should have been true!
                 *
                 * Instead use the isDisplayable
                 */
                if( isDisplayable() == true )
                {
                    try
                    {
                        /*******************************************************
                         * Note
                         *
                         * We execute the paintLayer in the EDT.
                         *
                         * If the layer is accessed by a non-EDT Thread
                         * the it needs to worry with synchronizing common
                         * resources used between these two threads.
                         * 
                         * The invokeAndWait ensure that we do not 
                         * overload/starve the system if we get
                         * behind taking too long in the paintLayers.
                         ******************************************************/
                        // Paint Layers
                        SwingUtilities.invokeAndWait( runner );
                    }
                    catch (InvocationTargetException ex)
                    {
                        ex.printStackTrace();
                    }
                    catch (InterruptedException ex)
                    {
                        ex.printStackTrace();
                    }
                }
            }
        };
        
        /*
         * Note that this uses fixed-delay execution
         */
        this.updateThread.schedule( timerTask, 2000, timerInterval );
        //this.updateThread.schedule( timerTask, 2000 );
    }
    
    
    /**
     * Stops the AnimatedComponent from Updating and Painting its Layers.  This
     * method stops and releases any resources created in the start
     * method.  Calling start after calling this method will result in
     * new resources being allocated.
     * <p>
     * This method can be called multiple times - calls after the 
     * first call are ignored.
     */
    public void stop( )
    {
        if( this.updateThread != null )
        {
            this.updateThread.cancel();
        }
        this.updateThread = null;
    }
    
    /**
     * Sets Frame Rate (should be called before start method).
     *
     * @param frameRate
     */
    public void setFrameRate( int frameRate )
    {
        this.frameRate = frameRate;
        /*
         * Convert framePerSecond to milli-seconds to sleep
         */
        this.timerInterval = new Float( 1000.0f / frameRate ).intValue();
    }
    
    /**
     * Gets Frame Rate.
     *
     * @return int frameRate specified in framesPerSecond
     */
    public int getFrameRate( )
    {
        return this.frameRate;
    }
    
    //--------------------------- Mouse Mode Methods -------------------------//
    /**
     * Sets the MouseMode.
     * 
     * @param newMouseMode
     */
    public void setMouseMode( MouseMode newMouseMode )
    {
        // Remove any previous and set to inactive
        MouseMode previousMouseMode = this.mouseMode;
        if( previousMouseMode != null )
        {
            this.removeMouseListener( previousMouseMode );
            this.removeMouseMotionListener( previousMouseMode );
            previousMouseMode.setActive( false );
        }
        
        this.mouseMode = newMouseMode;
        newMouseMode.setActive( true );
        this.addMouseListener( newMouseMode );
        this.addMouseMotionListener( newMouseMode );
        
        // Set our Cursor to that of the MouseMode
        this.setCursor( newMouseMode.getCursor() );
        
        // Let our listeners know
        this.fireMouseModeChanged( previousMouseMode, newMouseMode );
    }
    
    /******************************* Event Methods ****************************/
    
    /*********************************************************************
     * Notify all listeners that have registered interest for notification
     * on this event.
     ********************************************************************
     * @param oldMode
     * @param newMode
     */
    protected void fireMouseModeChanged( MouseMode oldMode, MouseMode newMode )
    {
        //Do this in order to only create Event once
        MouseModeEvent event = null;

        //Notify listeners last to first, notifying those that are interested
        for(int i = 0; i < this.mouseModeListeners.size(); i++)
        {
            // lazy initialization
            if(event == null)
            {
                // Determine min and max freq
                event = new MouseModeEvent(  this,
                                             (long)System.currentTimeMillis(),
                                             oldMode,
                                             newMode
                                           );
            }
            // Get listener
            MouseModeListener listener = ((MouseModeListener)this.mouseModeListeners.get(i));
            // Call listener
            listener.mouseModeChanged( event );
        }
    }
    
    /**
     * Adds MouseModeListener to list of listeners. First checks to see if
     * listener is already in list, if it is it does not add it a second time.
     * 
     * 
     * @param listener
     */
    public void addMouseModeListener( MouseModeListener listener )
    {
        if( !this.mouseModeListeners.contains(listener) )
        {
            this.mouseModeListeners.add(listener);
        }
    }

    /**
     * Removes MouseModeListener from list of listeners.
     * 
     * @param listener
     */
    public void removeMouseModeListener( MouseModeListener listener )
    {
        this.mouseModeListeners.remove(listener);
    }

    /**
     * Gets the MouseMode for the Chart.
     * 
     * @return MouseMode mouseMode
     */
    public MouseMode getMouseMode( )
    {
        return this.mouseMode;
    }
    //------------------------------------------------------------------------//
    
    
    
    /**
     * Sets the LayerModel.
     * 
     * 
     * @param newScanLayerModel
     */
    public void setLayerModel( LayerModel newScanLayerModel )
    {
        this.layerModel = newScanLayerModel;
    }
    
    /**
     * Gets this AnimatedComponent's LayerModel.
     * 
     * @return
     */
    public LayerModel getLayerModel( )
    {
        return this.layerModel;
    }
    
    
    //------------------------------------------------------------------------//
    // Overridden Component methods
    //------------------------------------------------------------------------//
    public void repaint( )
    {
        // ignore - we repaint the whole screen anyway
    }

    public void repaint( long  tm )
    {
        // ignore - we repaint the whole screen anyway
    }

    public void repaint( int  x, int  y, int  width, int  height )
    {
        // ignore - we repaint the whole screen anyway
    }

    public void repaint( long  tm, int x, int y, int width, int height )
    {
        // ignore - we repaint the whole screen anyway
    }

    public void repaint( Rectangle  r )
    {
        // ignore - we repaint the whole screen anyway
    }
    
    public void setEnabled( boolean enabled )
    {
    	super.setEnabled(enabled);
    	if( super.isEnabled() == true )
    	{
    		this.background.setColor( this.backgroundColor );
    	}
    	else
    	{
    		this.background.setColor( this.disabledColor );
    	}
    }
    //------------------------------------------------------------------------//
    
    /**
     * Calls paintLayer for each Layer.  Note that this method
     * assumes that it is on the EDT - do not call it directly
     * from any other thread than the EDT.
     */
    protected void paintLayers( )
    {
    	/*
         * Always assume that we want to render the entire component
         * region.  This has the benefit in that extending Layers do not
         * need to worry about this aspect - drawback is that we
         * would not be able to optimize for small dirty regions.
         */
        Rectangle repaintRegion = this.getBounds();
        
        //System.out.println( "paintImmediately " );
        /*
         * Call this.paintImmediately - the idea is that, since
         * we know we are on the EDT, this call immediately calls
         * this.paintComponent - which in turn calls the Layer's
         * paintLayer method.
         */
        this.paintImmediately( repaintRegion );
    }
    
    //////////////////////////////////////////////////////////////////////
    // overridden JComponent methods
    //////////////////////////////////////////////////////////////////////
    
    /**
     * The paintComponent method of JComponent is re-directed to call
     * the paintLayer method on each Layer.  The Layers do the actual
     * drawing of the graphics to this Component.
     * @param graphics
     */
    public void paintComponent( Graphics graphics )
    {
        //super.paintComponent(graphics);
        /*
         * Note that we do not call super.paintComponent which would
         * call the UIDelegates (if non-null) ui.update to paint
         * the background as opaque (if set) and then call the ui.paint
         * method.
         *
         * We do not have a UIDelegate (arguably the Layers are the
         * equivalent of a UIDelegate) - and we draw a background
         * making this component adhere to the "opaque=true" contract.
         */
        
        /*
         * Set a backgroundColor.  This is what makes us opaque!
         */
        this.background.paintLayer( this, ( Graphics2D ) graphics );
        
        /*
         * Set the default font for the Graphics from the Theme
         * Still struggling with this.  We act as a Button in order
         * to get the default colors and fonts.
         */
        Font defaultFont = UIManager.getFont( "Button.font" );
        //Color backgroundColor = UIManager.getColor( "Button.background" );
        //Color foregroundColor = UIManager.getColor( "Button.foreground" );
        graphics.setFont( defaultFont );
        
        /*
         * Enable anti-aliasing to get smooth outlines when drawing Strings.
         */
        ((Graphics2D)graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        
        /*
         * If we are disabled - we only paint the background layer
         */
        if( super.isEnabled() == true )
        {
	        /*
	         * Note that we call paintLayer in reverse
	         * order - the Layer at index [0] is "in front"
	         */
	        List <Layer> layers = this.layerModel.getLayers();
	        for (int i = layers.size() - 1; i >= 0; i--)
	        {
	            Layer layer = layers.get(i);
	            if( layer.isVisible() == true )
	            {
	                layer.paintLayer( this, ( Graphics2D ) graphics );
	            }
	        }
        }
    }

//    /**
//     * Size the graph wants to be
//     */
//    protected Dimension size = new Dimension( 0, 0 );
//
//    /**
//     * Sets the size
//     *
//     * @param Dimension dim
//     */
//    public void setSize( Dimension size )
//    {
//        this.size = size;
//    }
//
//    /**
//     * Gets the size
//     *
//     * @return Dimension dim
//     */
//    public Dimension getSize( )
//    {
//        return this.size;
//    }
//
//    /**
//     * Sets the preferredSize
//     *
//     * @param Dimension dim
//     */
//    public void setPreferredSize( Dimension size )
//    {
//        this.size = size;
//    }
//
//    /**
//     * Gets the preferredSize
//     *
//     * @return Dimension dim
//     */
//    public Dimension getPreferredSize( )
//    {
//        return this.size;
//    }
//
//
//    /**
//     * Sets the minimumSize
//     *
//     * @param Dimension dim
//     */
//    public void setMinimumSize( Dimension size )
//    {
//        this.size = size;
//    }
//
//    /**
//     * Gets the minimumSize
//     *
//     * @return Dimension dim
//     */
//    public Dimension getMinimumSize( )
//    {
//        return this.size;
//    }
//
//    /**
//     * Sets the maximumSize
//     *
//     * @param Dimension dim
//     */
//    public void setMaximumSize( Dimension size )
//    {
//        this.size = size;
//    }
//
//    /**
//     * Gets the maximumSize
//     *
//     * @return Dimension dim
//     */
//    public Dimension getMaximumSize( )
//    {
//        return this.size;
//    }
}

/*                                  ATELIER                                   */